package editor.animation;

import animation.*;
import animation.Character;
import com.crunch.core.*;
import com.crunch.core.Renderer;
import com.crunch.graphics.gpu.*;
import com.crunch.math.*;
import util.Camera;
import util.ShaderLoader;

import javax.media.opengl.GL;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.IntBuffer;
import java.nio.file.Path;
import java.util.ArrayList;

public class AnimationEditor extends GameState implements Renderable {
	public static void main(String[] args) {
		// start the engine
		new Engine(new Engine.Initializer() {
			@Override
			public void initialize(Engine engine) {
				// create the appropriate game.game states
				engine.createGameState(AnimationEditor.class, true);
			}
		});
	}

	public AnimationEditor() {
		super(0);
	}

	@Override
	public void onUpdate() {
		float newZoom = MathOps.lerp(camera.getZoom(), cameraZoom, 0.1f);
		camera.set(0.0f, 0.0f, newZoom);

		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				synchronized (AnimationEditor.this) {
					editor.frameBeginInPreview();
				}
			}
		});
	}

	@Override
	public void onCreate(Object... args) {
		// listen for quit button
		Input.EventListener listener = new Input.EventListener() {
			@Override
			public void processEvent(AWTEvent e, int eventType) {
				switch (eventType) {
					case Input.WINDOW_EVENT:
					{
						WindowEvent ev = (WindowEvent) e;
						if (ev.getID() == WindowEvent.WINDOW_CLOSING) {
							getEngine().shutdown();
						}
						break;
					}
					case Input.KEY_EVENT:
					{
						final KeyEvent ev = (KeyEvent) e;
						if (e.getID() == KeyEvent.KEY_PRESSED) {
							switch (ev.getKeyCode()) {
								case KeyEvent.VK_HOME:
									// reset camera
									cameraZoom = getEngine().window.getHeight() * 0.1f;
									break;
								case KeyEvent.VK_E:
									SwingUtilities.invokeLater(new Runnable() {
										@Override
										public void run() {
											editor.enableBoneInAnimationInPreview();
										}
									});
									break;
								case KeyEvent.VK_K:
									SwingUtilities.invokeLater(new Runnable() {
										@Override
										public void run() {
											editor.addKeyframeInPreview();
										}
									});
									break;
								case KeyEvent.VK_COMMA:
									SwingUtilities.invokeLater(new Runnable() {
										@Override
										public void run() {
											editor.prevKeyframeInPreview(ev.isControlDown());
										}
									});
									break;
								case KeyEvent.VK_PERIOD:
									SwingUtilities.invokeLater(new Runnable() {
										@Override
										public void run() {
											editor.nextKeyframeInPreview(ev.isControlDown());
										}
									});
									break;
							}
						}
						break;
					}
					case Input.MOUSE_WHEEL_EVENT:
					{
						MouseWheelEvent ev = (MouseWheelEvent) e;
						if (ev.getWheelRotation() > 0) {
							for (int i = 0; i < ev.getWheelRotation(); ++i) {
								cameraZoom *= (1.0f / 1.5f);
							}
						} else if (ev.getWheelRotation() < 0) {
							for (int i = 0; i < -ev.getWheelRotation(); ++i) {
								cameraZoom *= 1.5f;
							}
						}
						break;
					}
					case Input.MOUSE_EVENT:
					{
						MouseEvent ev = (MouseEvent) e;
						if (ev.getButton() == MouseEvent.BUTTON1) {
							if (ev.getID() == MouseEvent.MOUSE_PRESSED) {
								// send a select bone event to the editor
								Vector2f mouseView = new Vector2f((float) ev.getX(), (float) ev.getY());
								mouseView.divideAssign(new Vector2f(
										(float) camera.getViewportWidth(),
										(float) camera.getViewportHeight())) // map to [0,1]
										.multiplyAssign(2.0f) // map to [0,2]
										.subtractAssign(new Vector2f(1.0f, 1.0f)); // map to [-1,1]
								mouseView.y(-mouseView.y()); // flip y so that -1 is at bottom
								Vector2f mouseWorld = camera.screenToWorld(mouseView);

								// determine bones that were hit
								synchronized (AnimationEditor.this) {
									int boneIndex = -1;
									for (int i = 0; i < skeleton.getBoneCount(); ++i) {
										int index = skeleton.getDrawingOrderedBone(i);
										Skeleton.Bone bone = skeleton.getBone(index);
										Matrix33af tf = state.getBoneMatrices()[index];
										// transform mouseWorld into the space of tf
										Matrix33af tfInv = tf.invert();
										Vector2f mouseBone = tfInv.multiplyPoint(mouseWorld);
										// compare to the bone's length/thickness
										boolean hitBone = Math.abs(mouseBone.y()) <= bone.getThickness() &&
												mouseBone.x() >= 0.0f && mouseBone.x() <= bone.getLength();
										if (hitBone) {
											boneIndex = index;
										}
									}
									final int finalBoneIndex = boneIndex;
									SwingUtilities.invokeLater(new Runnable() {
										@Override
										public void run() {
											editor.selectBoneInPreview(finalBoneIndex);
										}
									});
									if (getEngine().input.isKeyDown(KeyEvent.VK_T)) {
										editMode = EditMode.TRANSLATE;
									} else if (getEngine().input.isKeyDown(KeyEvent.VK_R)) {
										editMode = EditMode.ROTATE;
									} else if (getEngine().input.isKeyDown(KeyEvent.VK_S)) {
										editMode = EditMode.SCALE;
									}
									mx = ev.getX();
									my = ev.getY();
									editSelection = boneIndex;
									if (editSelection >= 0) {
										initTranslation = editor.getBoneTranslation(editSelection);
										initRotation = editor.getBoneRotation(editSelection);
										initScale = editor.getBoneScale(editSelection);
										if (initTranslation == null) {
											editMode = EditMode.NONE;
										}
									}
								}
							} else if (ev.getID() == MouseEvent.MOUSE_RELEASED) {
								switch (editMode) {
									case TRANSLATE:
										editor.finishTranslateInPreview();
										break;
									case ROTATE:
										editor.finishRotateInPreview();
										break;
									case SCALE:
										editor.finishScaleInPreview();
										break;
								}
								editMode = EditMode.NONE;
							}
						}
						break;
					}
					case Input.MOUSE_MOTION_EVENT:
					{
						MouseEvent ev = (MouseEvent) e;
						if (ev.getID() == MouseEvent.MOUSE_DRAGGED) {
							final int dx = ev.getX() - mx;
							final int dy = ev.getY() - my;
							final boolean shift = ev.isShiftDown();
							final boolean ctrl = ev.isControlDown();
							synchronized (AnimationEditor.this) {
								if (editSelection < 0 || editSelection >= skeleton.getBoneCount()) {
									break;
								}

								// convert dx,dy vector into a world vector (from a screen vector)
								Vector2f dView = new Vector2f((float) dx, (float) dy);
								dView.divideAssign(new Vector2f(
										(float) camera.getViewportWidth(),
										(float) camera.getViewportHeight())) // map to [0,1]
										.multiplyAssign(2.0f);
								dView.y(-dView.y()); // flip y so that -1 is at bottom
								Vector2f dWorld = camera.screenToWorld(dView);

								switch (editMode) {
									case TRANSLATE:
										translate(dWorld, shift, ctrl);
										break;
									case ROTATE:
										rotate(dy, shift);
										break;
									case SCALE:
										scale(dWorld, shift, ctrl);
										break;
								}
							}
						}
						break;
					}
				}
			}
		};
		listener.eventMask = Input.ALL_EVENTS;
		getEngine().input.addEventListener(listener);

		camera = createActor(Camera.class);
		cameraZoom = getEngine().window.getHeight() * 0.1f;
		camera.set(0.0f, 0.0f, cameraZoom);

		setSkinSelection(-1);

		editor = new MainPanel(this);

		getEngine().window.renderer.queueCommand(new Renderer.Command() {
			@Override
			public void execute(GpuState gpuState) {
				synchronized (AnimationEditor.this) {
					if (skeletonRenderer == null) {
						ShaderLoader loader;
						try {
							loader = new ShaderLoader(
									getClass().getResource("/resources/programs/bone_vs.txt"),
									getClass().getResource("/resources/programs/bone_gs.txt"),
									getClass().getResource("/resources/programs/bone_fs.txt"));
						} catch (IOException e) {
							e.printStackTrace();
							System.exit(1);
							return;
						}
						GpuProgram.ProgramDescription desc = new GpuProgram.ProgramDescription();
						desc.vertexShaderSource = new String[]{loader.getVertexShaderSource()};
						desc.geometryShaderSource = new String[]{loader.getGeometryShaderSource()};
						desc.fragmentShaderSource = new String[]{loader.getFragmentShaderSource()};
						try {
							bonesProgram = gpuState.createProgram(desc);
						} catch (GpuException e) {
							System.out.println("Failed to load program");
							System.exit(1);
						}
						skeletonRenderer = new SkeletonRenderer(bonesProgram);
					}
					if (skinRenderer == null) {
						ShaderLoader loader;
						try {
							loader = new ShaderLoader(
									getClass().getResource("/resources/programs/skin_vs.txt"),
									getClass().getResource("/resources/programs/skin_gs.txt"),
									getClass().getResource("/resources/programs/skin_fs.txt"));
						} catch (IOException e) {
							e.printStackTrace();
							System.exit(1);
							return;
						}
						GpuProgram.ProgramDescription desc = new GpuProgram.ProgramDescription();
						desc.vertexShaderSource = new String[]{loader.getVertexShaderSource()};
						desc.geometryShaderSource = new String[]{loader.getGeometryShaderSource()};
						desc.fragmentShaderSource = new String[]{loader.getFragmentShaderSource()};
						try {
							skinProgram = gpuState.createProgram(desc);
						} catch (GpuException e) {
							System.out.println("Failed to load program");
							System.exit(-1);
						}
						skinRenderer = new SkinRenderer(skinProgram);
					}
				}
			}
		});

		getEngine().window.renderer.register(this);
	}

	@Override
	public void onDestroy() {
		editor.dispose();
		getEngine().window.renderer.unregister(this);
	}

	private MainPanel editor;

	// components of the character
	Path characterPath;

	Camera camera;
	float cameraZoom;
	SkeletonRenderer skeletonRenderer;
	SkinRenderer skinRenderer;
	GpuProgram bonesProgram;
	GpuProgram skinProgram;

	Skeleton skeleton;
	ArrayList<String> animationNames = new ArrayList<String>();
	ArrayList<Animation> animations = new ArrayList<Animation>();
	AnimationTree animationTree;
	AnimationStateMachine animationStateMachine;
	Character character;
	Character.State state;
	Vector4f[] boneColors;

	SkinComponentCollection skinComponentCollection;
	ArrayList<String> skinNames = new ArrayList<String>();
	ArrayList<Skin> skins = new ArrayList<Skin>();
	boolean refreshSkinImage;

	BufferedImage skinImage;
	GpuTexture skinTexture;
	SkinInstance skinInstance;
	int skinSelection;
	void setSkinSelection(int skinSelection) {
		this.skinSelection = skinSelection;
		if (skinSelection < 0) {
			skinInstance = null;
		} else {
			skinInstance = new SkinInstance(skins.get(skinSelection), skeleton, skinComponentCollection);
		}
	}

	enum EditMode {
		NONE,
		TRANSLATE,
		ROTATE,
		SCALE
	}
	EditMode editMode = EditMode.NONE;
	int editSelection;
	int mx, my;
	Vector2f initTranslation;
	float initRotation;
	Vector2f initScale;

	private void translate(Vector2f dWorld, boolean shift, boolean ctrl) {
		Matrix22f space;
		Skeleton.Bone bone = skeleton.getBone(editSelection);
		if (bone.getParent() < 0) {
			// space of root bone is identity transform
			space = new Matrix22f();
		} else {
			// space of bone is parent's transform (copy it since we will modify)
			Matrix33af par = state.getBoneMatrices()[bone.getParent()];
			space = new Matrix22f(par.get(0), par.get(1), par.get(3), par.get(4));
		}
		// space now contains the "x" and "y" axes of the parent's transformation
		// get dWorld in terms of these axes:
		// x*xAxis + y*yAxis = dWorld
		// space * [x,y] = dWorld
		// [x,y] = space^-1 * dWorld
		if (space.invertAssign() == null) {
			return; // at least one axis is 0 - can't do anything meaningful
		}
		final Vector2f xy = space.multiply(dWorld);
		if (shift) {
			xy.multiplyAssign(0.1f);
		}
		// restrict to one axis if ctrl is pressed
		if (ctrl) {
			if (Math.abs(xy.x()) > Math.abs(xy.y())) {
				xy.y(0.0f);
			} else {
				xy.x(0.0f);
			}
		}
		xy.addAssign(initTranslation);

		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				editor.translateInPreview(xy);
			}
		});
	}

	private void rotate(int dy, boolean shift) {
		float rotation = (float) (-dy) * ((float) Math.PI * 2.0f);
		rotation *= shift ? 0.0005f : 0.005f;
		float newRot = (initRotation + rotation) % ((float) Math.PI * 2.0f);
		if (newRot < 0.0f) {
			newRot += ((float) Math.PI * 2.0f);
		}

		final float fRot = newRot;
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				editor.rotateInPreview(fRot);
			}
		});
	}

	private void scale(Vector2f dWorld, boolean shift, boolean ctrl) {
		Matrix22f space;
		Skeleton.Bone bone = skeleton.getBone(editSelection);
		if (bone.getParent() < 0) {
			// space of root bone is identity transform, rotated by its rotation (since scale is applied after rotation)
			space = new Matrix22f();
		} else {
			// space of bone is parent's transform, rotated by bone's rotation
			Matrix33af par = state.getBoneMatrices()[bone.getParent()];
			space = new Matrix22f(par.get(0), par.get(1), par.get(3), par.get(4));
		}
		// apply child bone rotation by post-multiplying with the child's rotation
		space.multiplyAssign(new Matrix22f().rotateAssign(bone.getRotation()));

		// space now contains the "x" and "y" axes of the parent's transformation
		// get dWorld in terms of these axes:
		// x*xAxis + y*yAxis = dWorld
		// space * [x,y] = dWorld
		// [x,y] = space^-1 * dWorld
		if (space.invertAssign() == null) {
			return; // at least one axis is 0 - can't do anything meaningful
		}
		final Vector2f xy = space.multiply(dWorld);
		if (shift) {
			xy.multiplyAssign(0.1f);
		}
		if (ctrl) {
			if (Math.abs(xy.x()) > Math.abs(xy.y())) {
				xy.y(0.0f);
			} else {
				xy.x(0.0f);
			}
		}
		xy.addAssign(initScale);

		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				editor.scaleInPreview(xy);
			}
		});
	}

	AnimationEvaluator evaluator = new AnimationEvaluator();

	public void render(GpuState gpuState) {
		synchronized (this) {
			if (refreshSkinImage) {
				if (skinTexture != null) {
					skinTexture.release();
					skinTexture = null;
				}
				if (skinImage != null) {
					GpuTexture.Texture2DDescription desc = new GpuTexture.Texture2DDescription();
					desc.imageDescription.internalFormat = GL.GL_SRGB;
					desc.imageDescription.minFilter = GL.GL_NEAREST;
					desc.imageDescription.magFilter = GL.GL_NEAREST;
					GpuTexture.Data2DDescription mipLevel = new GpuTexture.Data2DDescription();
					desc.mipmapLevels = new GpuTexture.Data2DDescription[] { mipLevel };
					mipLevel.width = skinImage.getWidth();
					mipLevel.height = skinImage.getHeight();
					mipLevel.format = GL.GL_RGBA;
					mipLevel.type = GL.GL_UNSIGNED_BYTE;
					int[] rgbaData = skinImage.getRGB(0, 0, skinImage.getWidth(), skinImage.getHeight(), null, 0, skinImage.getWidth());
					// swap r and b bytes
					for (int i = 0; i < rgbaData.length; ++i) {
						rgbaData[i] =
								(rgbaData[i] & 0xFF00FF00) | (Integer.rotateLeft(rgbaData[i], 16) & 0x00FF00FF);
					}
					mipLevel.data = IntBuffer.wrap(rgbaData);
					desc.wrapS = GL.GL_CLAMP_TO_EDGE;
					desc.wrapT = GL.GL_CLAMP_TO_EDGE;
					skinTexture = gpuState.createTexture(desc);
				}
				refreshSkinImage = false;
			}

			GpuDrawCall.ClearCall cc = gpuState.createClearCall();
			cc.setClearBuffers(GL.GL_COLOR_BUFFER_BIT);
			cc.setClearColor(new float[] { 0.0f, 0.0f, 0.0f, 0.0f });
			cc.clear();

			if (skinInstance != null && skinTexture != null) {
				skinRenderer.drawSkin(gpuState, camera.getViewProjectionMatrix(), state, skinInstance, skinTexture, new Vector4f(1.0f, 1.0f, 1.0f, 1.0f));
			}

			skeletonRenderer.drawSkeleton(gpuState, camera.getViewProjectionMatrix(), state, boneColors);
		}
	}

	public int getDepth() {
		return 0;
	}
}